package com.gotobject.junit.internal.runners.conditions;

import com.gotobject.junit.internal.runners.model.ConditionInvocation;
import com.gotobject.junit.internal.runners.model.Explosion;
import com.gotobject.junit.internal.runners.model.PerformanceKey;
import com.gotobject.filter.condition.Condition;
import com.gotobject.filter.condition.AbstractCondition;
import org.junit.runner.Description;
import org.junit.runner.notification.Failure;
import org.junit.runner.notification.RunNotifier;
import org.junit.runners.model.FrameworkMethod;

import java.lang.reflect.Constructor;
import java.util.List;
import java.util.ArrayList;
import java.util.concurrent.atomic.AtomicBoolean;

/**
 * a sacred place where all of the conditions ruling the world are written :).
 * @author Huascar A. Sanchez (huascar.sanchez@gmail.com)
 */
public class ConditionMaker {
    private ConditionMaker(){
        throw new UnsupportedOperationException();
    }


	/**
     * create a new condition that given a list of explosions will be able confirm
     * if the test is in autocruised (loaded from an xml file).
     * @param method test method.
	 * @param exp or list of explosions.
     * @return {@code true} if we are dealing with autocruised.
     */
	public static boolean autocruised(FrameworkMethod method, List<Explosion> exp) {
		return with(method, exp).mix(unknownPerformanceKey(), fullOfExplosions());
	}

    /**
     * returns {@code true} if the test should be executed. False otherwise.
     * @param desc test case.
     * @param notifier run notifier
     * @param cond condition to be invoked.
     * @return {@code true} if the test should be executed.
     */
    public static boolean executed(Description desc, RunNotifier notifier, ConditionInvocation cond){
        return expectedOutcome(desc, notifier).isSatisfied(cond);
    }

	/**
     * returns a condition that is being satisfied by a {@link ConditionInvocation}.
	 * @param description or test method being executed by {@code JUnit}
	 * @param notifier the current run notifier being used by {@code JUnit}.
	 * @return a {@link Condition<ConditionInvocation>}.
     */
    public static Condition<ConditionInvocation> expectedOutcome(final Description description, final RunNotifier notifier){
        return new AbstractCondition<ConditionInvocation>(){
			private ConditionInvocation e = ConditionInvocation.NULL_ONE;
			public boolean isSatisfied(ConditionInvocation thing) {
				if(thing == null) throw new IllegalArgumentException("params cannot be null.");
				e = thing;
				final AtomicBoolean result = new AtomicBoolean(false);
                try {
                    final Object ifReturn = thing.invoke();
                    result.set(ifReturn.getClass().equals(Boolean.class) && (Boolean.class.cast(ifReturn) && thing.getExpectedOutcome()));
                } catch (Exception ex) {
					final Exception internalEx = makeException(ex);
					if(notifier != null){
						notifier.fireTestFailure(new Failure(description, internalEx));
					}
                }
                return result.get();
            }

            @Override
            public String toString() {
                return "expectedOutcome for(" + e + ")";
            }
        };
    }

	/**
     * creates a condition which indicates whether a test is full of explosions to
     * be executed.
     * @return a {@link Condition} object.
     */
	public static Condition<List<Explosion>> fullOfExplosions(){
		return new AbstractCondition<List<Explosion>>(){
			private final List<Explosion> result = new ArrayList<Explosion>();
			public boolean isSatisfied(List<Explosion> thing) {
				result.addAll(thing);
				return !result.isEmpty();
			}

            @Override
            public String toString() {
                return " full Of Explosions " + result;
            }
		};
	}

	/**
     * creates a condition which indicates whether a test is full of explosions to
     * be executed.
     * @param exp list of explosions.
     * @return a {@link Condition} object.
	 * @deprecated see {@link #fullOfExplosions()}
     */
    public static Condition<FrameworkMethod> fullOfExplosions(final List<Explosion> exp){
        return new AbstractCondition<FrameworkMethod>(){
            private String testName = "none";
            public boolean isSatisfied(FrameworkMethod thing) {
                testName = thing.getName();
                return !exp.isEmpty();
            }

            @Override
            public String toString() {
                return testName + " is full Of Explosions " + exp.toString();
            }
        };

    }


    /**
     * creates a new condition that indicates if a test corresponds to a unknown performance key test.
     * @return a {@link Condition} object applied to an {@link Explosion}.
     */
    public static Condition<Explosion> likelyUnknownPerformanceKey(){
        return new AbstractCondition<Explosion>(){
            private Explosion explosion = new Explosion(0, 0, 0L);
            public boolean isSatisfied(Explosion thing) {
                explosion = thing;
                return PerformanceKey.like(thing.getTestType()) == PerformanceKey.UNKNOWN;
            }

            @Override
            public String toString() {
                return "unknown performance key for(" + explosion + ")";
            }
        };
    }


	/**
	 * indicates if there is chance we are dealing with a manual explosion (in other words
	 * we are using the perf testing's annotations like {@literal @Load})
	 * @param exp explosion to be checked.
	 * @return {@code true} if this is a manual explosion.
	 */
	public static boolean likelyManualExplosion(Explosion exp){
		return likelyUnknownPerformanceKey().isSatisfied(exp);
	}


    /**
     * creates an exception which will wrap a thrown {@link Throwable error}.
     * @param t a thrown {@link Throwable error}.
     * @return an {@link Exception}.
     */
    private static Exception makeException(Throwable t) {
        try {
            final Constructor<Exception> c = Exception.class.getDeclaredConstructor(String.class, Throwable.class);
            return c.newInstance("Internal error when checking prerequisites.", t);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * negates a given condition.
	 * @param p is the {@link Condition} to be negated.
	 * @return the negated {@link Condition}.
	 */
    public static <T> Condition<T> not(final Condition<T> p) {
        if(p == null) throw new IllegalArgumentException("params should not be null, period!");
        return new AbstractCondition<T>() {
            public boolean isSatisfied(T t) {
                return !p.isSatisfied(t);
            }

            @Override
            public String toString() {
                return "not(" + p + ")";
            }
        };
    }


	/**
	 * indicates if we are dealing with performance testing runners or just regular
	 * unit testing runners.
	 * @param method test method.
	 * @param exp list of explosions.
	 * @return {@code true} if we will be giving up control to the normal Junit's runners.
	 */
	public static boolean skippedAll(FrameworkMethod method, List<Explosion> exp) {
		return with(method, exp).mix(unknownPerformanceKey(), not(fullOfExplosions()));
	}

	/**
     * create a mixin of heterogenous conditions taking a user-defined {@literal T} and {@literal E}.
     * @param t   concreate value of T
     * @param e   concrete value of E
     * @param <T> user-defined type
     * @param <E> user-defined type
     * @return a {@link ConditionMixin} object.
     */
    public static <T, E> ConditionMixin<T, E> with(final T t, final E e) {
		 if(t == null) throw new IllegalArgumentException("params should not be null, period!");
		 if(e == null) throw new IllegalArgumentException("params should not be null, period!");
		return new ConditionMixin<T, E>(){
            public boolean mix(Condition<T> one, Condition<E> two) {
                return one.isSatisfied(t) && two.isSatisfied(e);
            }
        };
    }

	/**
     * create a new condition that indicates if a test corresponds to a unknown performance key test.
     * @return a {@link Condition} object.
     */
    public static Condition<FrameworkMethod> unknownPerformanceKey(){
        return new AbstractCondition<FrameworkMethod>(){
            private FrameworkMethod targetMethod = new FrameworkMethod(null);
            public boolean isSatisfied(FrameworkMethod thing) {
                targetMethod = thing;
                return PerformanceKey.from(thing) == PerformanceKey.UNKNOWN;
            }

            @Override
            public String toString() {
                return "unknown performance key for(" + targetMethod.getName() + ")";
            }
        };
    }
}
